/**
   glut_display.cu - Created by Timothy Morey on 8/9/2011.

 */

#include <GL/freeglut.h>
#include <pthread.h>
#include <stdio.h>
#include <string>

#define _USE_MATH_DEFINES
#include <math.h>

#include <boost/timer.hpp>

#include "glut_display.h"
#include "simulator.h"

using namespace std;

GlutDisplay* GlutDisplay::_Instance = 0;

GlutDisplay::GlutDisplay()
  : _FullScreenMode(false),
    _SimpleRendering(true),
    _ThreadID(0),
    _TimeCounts(0),
    _ElapsedSum(0.0),
    _AverageTime(0.0)
{
}

GlutDisplay::~GlutDisplay()
{
  glutLeaveMainLoop();

  if(_ThreadID)
    pthread_join(_ThreadID, 0);
}

GlutDisplay& GlutDisplay::Instance()
{
  if(! _Instance)
  {
    _Instance = new GlutDisplay();
  }

  return *_Instance;
}

void GlutDisplay::DestroyInstance()
{
  if(_Instance)
  {
    delete _Instance;
    _Instance = 0;
  }
}

void GlutDisplay::Init(int x, int y, int width, int height, const string& name)
{
  _MainWindow.X = x;
  _MainWindow.Y = y;
  _MainWindow.Width = width;
  _MainWindow.Height = height;
  _MainWindow.Name = name;
  
  pthread_create(&_ThreadID, 0, GlutDisplay::StartGlutLoop, 0);
}

void GlutDisplay::UpdateScreenBounds()
{
  if(pthread_self() == GlutDisplay::Instance()._ThreadID)
  {
    GlutWindow& mainWindow = GlutDisplay::Instance()._MainWindow;
    glutSetWindow(mainWindow.WindowID);

    //glMatrixMode(GL_PROJECTION);
    //glLoadIdentity();
    //const Parameters& parms = Simulator::Instance().GetParameters();
    //glOrtho(parms.MinX, parms.MaxX, parms.MinY, parms.MaxY, -1.0, 1.0);
    //glOrtho(0, Simulator::Instance().GetFrameWidth(),
    //        0, Simulator::Instance().GetFrameHeight(),
    //        -1.0, 1.0);
    //glMatrixMode(GL_MODELVIEW);
  }
  else
  {
    _Queue.Push(new Message("UpdateScreenBounds", 0));
  }
}

void GlutDisplay::PrintFrameRateReport() const
{
  if(_AverageTime == 0.0)
    printf("No frame rate data to report\n");
  else
    printf("Average frame render time: %6.4f s, Frames per second: %6.1f\n",
           _AverageTime, 1.0 / _AverageTime);
}

void* GlutDisplay::StartGlutLoop(void *arg)
{
  int argc = 1;
  char *argv = 0;
  glutInit(&argc, &argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

  GlutWindow& mainWindow = GlutDisplay::Instance()._MainWindow;
  glutInitWindowSize(mainWindow.Width, mainWindow.Height);
  glutInitWindowPosition(mainWindow.X, mainWindow.Y);
  mainWindow.WindowID = glutCreateWindow(mainWindow.Name.c_str());

  GlutDisplay::Instance().UpdateScreenBounds();
  
  glutDisplayFunc(GlutDisplay::DisplayFunc);
  glutIdleFunc(GlutDisplay::IdleFunc);
  glutKeyboardFunc(GlutDisplay::KeyboardFunc);
  glutReshapeFunc(GlutDisplay::ReshapeFunc);

  glutMainLoop();

  return 0;
}

void GlutDisplay::DisplayFunc()
{
  GlutWindow& mainWindow = GlutDisplay::Instance()._MainWindow;
  glutSetWindow(mainWindow.WindowID);
  glClear(GL_COLOR_BUFFER_BIT);

  boost::timer timer;

  Simulator& sim = Simulator::Instance();

  glDrawPixels(sim.GetFrameWidth(), sim.GetFrameHeight(),
               GL_RGBA, GL_UNSIGNED_BYTE, sim.GetFrame());
  
  // for(int i = 0; i < sim.GetParameters().N; i++)
  //   GlutDisplay::Instance().DrawParticle(sim.GetX(i), sim.GetY(i), sim.GetVM(i),
  //       				 sim.GetParameters());

  GlutDisplay& display = GlutDisplay::Instance();
  display._ElapsedSum += timer.elapsed();
  display._TimeCounts++;

  if(display._TimeCounts == 100)
  {
    display._AverageTime = 
      display._ElapsedSum / 100.0;
    display._TimeCounts = 0;
    display._ElapsedSum = 0.0;
  }
  
  glutSwapBuffers();
}

void GlutDisplay::IdleFunc()
{
  while(! GlutDisplay::Instance()._Queue.IsEmpty())
  {
    MessagePtr msg = GlutDisplay::Instance()._Queue.Pop();
    if(msg && msg->GetMessage() == "UpdateScreenBounds")
    {
      GlutDisplay::Instance().UpdateScreenBounds();
    }
    
    if(msg)
      msg->MarkAsHandled();
  }

  glutPostRedisplay();
}

void GlutDisplay::KeyboardFunc(unsigned char key, int x, int y)
{
  switch(key)
  {
  case 's':
  case 'S':
    if(Simulator::Instance().IsRunning())
      Simulator::Instance().Stop();
    else
      Simulator::Instance().Start(); // <NOTE: starting from gui thread here...
    break;

  case 'p':
  case 'P':
    if(Simulator::Instance().PauseFlag())
      Simulator::Instance().Resume();
    else
      Simulator::Instance().Pause();
    break;

  case 'q':
  case 'Q':
    GlutDisplay::DestroyInstance();
    Simulator::DestroyInstance();
    break;

  case 'f':
  case 'F':
    GlutDisplay::Instance().ToggleFullScreenMode();
    break;

  case 'r':
  case 'R':
    GlutDisplay::Instance().ToggleSimpleRendering();
    break;

  }
}

void GlutDisplay::ReshapeFunc(int width, int height)
{
  glPixelZoom((float)width / (float)Simulator::Instance().GetFrameWidth(),
              (float)height / (float)Simulator::Instance().GetFrameHeight());
}

void GlutDisplay::DrawParticle(double x, double y, double v, const Parameters& parms)
{
  double r = 0.25;

  this->DrawCircle(x, y, r, v);

  if(x - r < parms.MinX)
    this->DrawCircle(x + (parms.MaxX - parms.MinX), y, r, v);
  else if(x + r > parms.MaxX)
    this->DrawCircle(x - (parms.MaxX - parms.MinX), y, r, v);

  if(y - r < parms.MinY)
    this->DrawCircle(x, y + (parms.MaxY - parms.MinY), r, v);
  else if(y + r > parms.MaxY)
    this->DrawCircle(x, y - (parms.MaxY - parms.MinY), r, v);
}

void GlutDisplay::DrawCircle(double x, double y, double r, double v)
{
  if(_SimpleRendering)
  {
    glColor3ub(0, 200, 0);
    glBegin(GL_POLYGON);
    {
      glVertex2d(x + r, y + r);
      glVertex2d(x - r, y + r);
      glVertex2d(x - r, y - r);
      glVertex2d(x + r, y - r);
    }
    glEnd();
  }
  else
  {
    glColor3ub(0 + (min(5.0, v) * 51.0), 0, 255 - (min(5.0, v) * 51.0));
    glBegin(GL_POLYGON);
    {
      for(double theta = 0.0; theta < 2.0 * M_PI; theta += M_PI / 16.0)
      {
        glVertex2d(x + r * cos(theta), y + r * sin(theta));
      }
    }
    glEnd();
  }
}

void GlutDisplay::ToggleFullScreenMode()
{
  // NOTE: assuming this call came in on the UI thread...

  glutSetWindow(_MainWindow.WindowID);
  _FullScreenMode = !_FullScreenMode;
  if(_FullScreenMode)
  {
    glutFullScreen();
    glutSetCursor(GLUT_CURSOR_NONE);
  }
  else
  {
    glutPositionWindow(0, 0);
    glutSetCursor(GLUT_CURSOR_LEFT_ARROW);
  }
}

void GlutDisplay::ToggleSimpleRendering()
{
  // NOTE: assuming this call came in on the UI thread...

  _SimpleRendering = !_SimpleRendering;
}
